/*
 * Copyright 2011 Christoph Ludwig (christoph[at]chludwig.de). All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.chludwig.ehumanities.provenance.persistence;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.transform.stream.StreamSource;

import de.chludwig.ehumanities.provenance.entities.WorkflowContainer;
import de.chludwig.ehumanities.provenance.exception.InitializationException;
import de.chludwig.ehumanities.provenance.exception.ProvenanceFileNotFoundException;
import de.chludwig.ehumanities.provenance.exception.ProvenanceIOException;
import de.chludwig.ehumanities.provenance.exception.ProvenanceJAXBException;
import de.chludwig.ehumanities.provenance.jaxb.MarshallerFactory;
import de.chludwig.ehumanities.provenance.jaxb.ObjectFactory;
import de.chludwig.ehumanities.provenance.jaxb.UnmarshallUtil;
import de.chludwig.ehumanities.provenance.jaxb.WorkflowContainerMetadataType;
import de.chludwig.ehumanities.provenance.utils.FileUtil;
import de.chludwig.ehumanities.provenance.utils.StreamUtil;

/**
 * Peristence object for workflow containers.
 */
public class WorkflowContainerPO {

  public static final String METADATA_FILE_NAME = "metadata";
  
  public static final String ENTRIES_FILE_NAME = "entries";

  public static final String README_FILE_NAME = "README";

  public static final ByteArrayOutputStream readmeBuffer = createReadmeBuffer();
  
  private class EntryFileFilter implements FileFilter {

    private WorkflowContainerPO containerPO;
    
    public EntryFileFilter(WorkflowContainerPO containerPO) {
      this.containerPO = containerPO;
    }
    
    @Override
    public boolean accept(File candidate) {
      return LogEntryPO.isLogEntryFolder(containerPO, candidate);
    }
  
  };

  private StorageArea storageArea;
  private UUID containerId;
  private File containerFolder;

  
  private File entriesFolder;

  public WorkflowContainerPO(StorageArea storageArea, UUID containerId) {
    this.storageArea = storageArea;
    this.containerId = containerId;
    this.containerFolder = null;
  }

  /**
   * @return the storageArea
   */
  public StorageArea getStorageArea() {
    return storageArea;
  }

  /**
   * @return the containerId
   */
  public UUID getContainerId() {
    return containerId;
  }
  
  public File getContainerFolder() {
    if(containerFolder == null) {
      File stagingFolder = storageArea.getStagingFolder();
      containerFolder = new File(stagingFolder, containerId.toString());
    }
    return containerFolder;
  }
  
  /**
   * @return
   */
  public File getEntriesFolder() {
    if (entriesFolder == null) {
      entriesFolder = new File(getContainerFolder(), ENTRIES_FILE_NAME);
    }
    return entriesFolder;
  }

  /**
   * @param containerFolder
   * @return
   */
  public File getMetadataFile() {
    return new File(getContainerFolder(), METADATA_FILE_NAME);
  }

  public void persistContainer(WorkflowContainer container) {
    try {
      File folder = getContainerFolder();
      makeEmptyFolder(folder);
      
      writeReadmeFile();
      writeMetadataFile(container);
      createEntriesFolder();
    }
    catch (IOException ioexception) {
      throw new ProvenanceIOException("cannot create workflow container", ioexception);
    }
    catch (JAXBException jaxbexception) {
      throw new ProvenanceJAXBException("cannot create workflow container", jaxbexception);
    }

  }

  /**
   * @param containerFolder
   * @return
   * @throws JAXBException
   */
  public WorkflowContainerMetadataType readMetadata() {
    try {
      File metadataFile = getReadableMetadataFile();
      StreamSource source = new StreamSource(metadataFile);
      JAXBElement<WorkflowContainerMetadataType> elem = UnmarshallUtil.workflowContainerMetadataElement(source);
      return elem.getValue();
    }
    catch (JAXBException ex) {
      throw new ProvenanceJAXBException("cannot read container metadata", ex);
    }
  }

  public List<UUID> getLogEntryIds() {
    List<UUID> result = new ArrayList<UUID>();
    EntryFileFilter fileFilter = new EntryFileFilter(this);
    for (File entryFolder : getEntriesFolder().listFiles(fileFilter)) {
      UUID entryId = UUID.fromString(entryFolder.getName());
      result.add(entryId);
    }
    return result;
  }

  public ArchivePO toArchive() {
    ArchivePO archivePO = new ArchivePO(getStorageArea(), getContainerId());
    File sourceFolder = getContainerFolder();
    File targetFolder = archivePO.getArchiveFolder();
    targetFolder.getParentFile().mkdirs();
    sourceFolder.renameTo(targetFolder);
    return archivePO;
  }

  public void sweepIfStale(Date cutoffTimestamp) {
    File folder = getContainerFolder();
    if (!FileUtil.treeModifiedSince(folder, cutoffTimestamp.getTime())) {
      File tmpArea = getStorageArea().getTmpFolder();
      File tmpFile = new File(tmpArea, folder.getName());
      folder.renameTo(tmpFile);
      FileUtil.deleteRecursively(tmpFile);
    }
  }

  /**
   * @param folder
   */
  private void makeEmptyFolder(File folder) {
    folder.mkdirs();
    File[] elems = folder.listFiles();
    if(elems == null) {
      throw new ProvenanceIOException("container folder " + folder.getAbsolutePath() + " is not a readable directory", null);
    }
    for(File elem : Arrays.asList(elems)) {
      if(!FileUtil.deleteRecursively(elem)) {
        throw new ProvenanceIOException("could not delete all existing files and directories in " + folder.getAbsolutePath(), null);
      }
    }
  }

  /**
   * @throws FileNotFoundException
   * @throws IOException
   * @throws PropertyException
   * @throws JAXBException
   */
  private void writeMetadataFile(WorkflowContainer container) throws FileNotFoundException, IOException, PropertyException, JAXBException {
    Marshaller marshaller = MarshallerFactory.createMarshaller(WorkflowContainerMetadataType.class);
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    File metadataFile = getMetadataFile();
    JAXBElement<WorkflowContainerMetadataType> root = jaxbMetadataRootElement(container.getMetadata());
    marshaller.marshal(root, metadataFile);
  }

  /**
   * @param container
   * @return
   */
  private JAXBElement<WorkflowContainerMetadataType> jaxbMetadataRootElement(WorkflowContainerMetadataType metadata) {
    JAXBElement<WorkflowContainerMetadataType> root = new ObjectFactory()
            .createWorkflowContainerMetadata(metadata);
    return root;
  }

  /**
   * @throws FileNotFoundException
   * @throws IOException
   */
  private void writeReadmeFile() throws FileNotFoundException, IOException {
    File readmeFile = new File(getContainerFolder(), README_FILE_NAME);
    OutputStream readmeOutputStream = new BufferedOutputStream(new FileOutputStream(readmeFile));
    try {
      readmeBuffer.writeTo(readmeOutputStream);
    }
    finally {
      readmeOutputStream.close();
    }
  }

  /**
   * @throws IOException
   */
  private void createEntriesFolder() throws IOException {
    File folder = getEntriesFolder();
    if (!folder.mkdir()) {
      throw new IOException("cannot create directory " + folder.getAbsolutePath());
    }
  }

  private File getReadableMetadataFile() {
    File metadataFile = getMetadataFile();
    if (!(metadataFile.isFile() && metadataFile.canRead())) {
      String msg = "metadata file " + metadataFile + " does not exist or is not a readable regular file";
      throw new ProvenanceFileNotFoundException(msg, null).checkIfArchived(getContainerId());
    }    
    return metadataFile;
  }

  /**
   * Copies the content of the container README file into memory.
   *  
   * @return a ByteArrayOutputStream the content of the README file (and nothing else) was written to.
   * @throws IOException reading of the README file failed.
   */
  private static ByteArrayOutputStream createReadmeBuffer() {
    ByteArrayOutputStream readmeBuffer = new ByteArrayOutputStream();
    InputStream readmeStream = WorkflowContainerPO.class.getClassLoader().getResourceAsStream(README_FILE_NAME);
    try {
      try {
        new StreamUtil().transfer(readmeStream, readmeBuffer);
      }
      finally {
        readmeStream.close();
      }
    }
    catch (IOException ex) {
      throw new InitializationException("Could not create README file buffer", ex);
    }
    return readmeBuffer;
  }
}
